Building Searchable and Filterable Lists
Course: iOS App Development
Topic: Building Searchable and Filterable Lists
Course Link: iOS Training at JustAcademy
Demo Registration: Register for a Course Demo
JustAcademy Homepage: Visit JustAcademy
1. Introduction to Searchable and Filterable Lists
Searchable and filterable lists are important features in modern iOS applications. They allow users to locate specific records quickly instead of manually scrolling through a large collection of items. Examples include contact directories, product catalogs, music libraries, message histories, employee directories, and task management applications.
In UIKit, developers commonly use UISearchController, UISearchBar, UITableView, and UICollectionView to implement search interfaces. A search operation identifies records that match text entered by the user, while filtering restricts the displayed records according to selected conditions such as category, status, price, or date.
These operations can be combined. For example, an online store may allow users to search for a product by name and then filter the results by category or availability.
Learning Objectives
- Understand the difference between searching and filtering.
- Learn how to integrate
UISearchController with a table view.
- Implement real-time search using
UISearchResultsUpdating.
- Filter data by text, category, and status.
- Combine multiple filtering conditions.
- Implement sorting, empty states, and clear-search behavior.
- Understand search performance, accessibility, and testing.
2. Understanding Search and Filtering
Search and filtering are related but distinct operations. Search generally uses text or keywords to find matching records. Filtering applies conditions that determine which records should remain visible.
| Feature |
Definition |
Example |
| Search |
Finds records matching a search query. |
Finding a product named “Wireless Headphones”. |
| Filtering |
Restricts records according to specific conditions. |
Showing only products in the Electronics category. |
| Sorting |
Arranges matching records in a chosen order. |
Sorting products by price from lowest to highest. |
| Combined search and filtering |
Applies keyword matching and additional conditions together. |
Finding headphones priced below a specified amount. |
3. UIKit Components Used for Search
3.1 UISearchController
UISearchController coordinates a search interface and the presentation of search results. It manages search interaction and informs the configured results updater when the search information changes. It can work with the same view controller that displays the original content or with a separate results controller.
3.2 UISearchBar
UISearchBar is the interface where users enter search text. It includes a text field and can provide search and cancel controls. When used with a search controller, it is normally accessed through the controller's searchBar property.
3.3 UISearchResultsUpdating
UISearchResultsUpdating is a protocol used to respond when search information changes. The method updateSearchResults(for:) provides an opportunity to recalculate matching records and update the interface.
3.4 UITableView
UITableView displays records in rows. It is commonly used for search results that primarily contain text and structured details.
3.5 UICollectionView
UICollectionView displays search results in grids, cards, galleries, and other custom arrangements. It is suitable when each result contains images or richer visual content.
| Component |
Main Responsibility |
UISearchController |
Coordinates search interaction and result presentation. |
UISearchBar |
Collects the user's search input. |
UISearchResultsUpdating |
Notifies the app when search results should be updated. |
UITableView |
Displays results in rows. |
UICollectionView |
Displays results using a flexible layout. |
UISegmentedControl |
Can provide simple filter choices, such as All, Active, and Completed. |
4. Creating a Data Model for Search
Before implementing search, define the data that users will search. A well-structured model makes it easier to match records by multiple properties and add new filtering options later.
import Foundation
struct Product: Identifiable {
let id: UUID
let name: String
let category: String
let price: Double
let isAvailable: Bool
init(
id: UUID = UUID(),
name: String,
category: String,
price: Double,
isAvailable: Bool = true
) {
self.id = id
self.name = name
self.category = category
self.price = price
self.isAvailable = isAvailable
}
}
Understanding the Model
id uniquely identifies a product.
name contains the text used for name-based searches.
category supports category filtering.
price allows price-based filtering and sorting.
isAvailable indicates whether the product is available.
Keeping the model separate from the user interface allows the same data to be displayed in different views and filtered using different rules.
5. Preparing Sample Data
The following array provides sample records for a product list. In a production application, the data may come from a local database, a web service, or another repository.
let products = [
Product(
name: "Wireless Headphones",
category: "Electronics",
price: 2499
),
Product(
name: "Bluetooth Speaker",
category: "Electronics",
price: 1799
),
Product(
name: "Running Shoes",
category: "Fashion",
price: 2999
),
Product(
name: "Travel Backpack",
category: "Accessories",
price: 1499
),
Product(
name: "Smart Watch",
category: "Electronics",
price: 3999,
isAvailable: false
)
]
For predictable behavior, maintain a clear separation between the original dataset and the currently displayed results. This prevents each search from accidentally discarding records that may match a later query.
6. Implementing Basic Text Search
For a small dataset, Swift's collection filtering methods provide a straightforward way to find matching records. The following example searches product names without treating uppercase and lowercase letters as different.
func searchProducts(
_ products: [Product],
query: String
) -> [Product] {
let searchText = query.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !searchText.isEmpty else {
return products
}
return products.filter {
$0.name.localizedCaseInsensitiveContains(searchText)
}
}
How It Works
- Remove unnecessary spaces at the beginning and end of the query.
- Check whether the query is empty.
- If it is empty, return the original list.
- Otherwise, keep products whose names contain the query using a case-insensitive localized comparison.
For example, searching for head returns “Wireless Headphones”. Searching for SMART can return “Smart Watch” without requiring the user to match the original capitalization.
7. Filtering by Category
Category filters let users restrict results to a specific group. This is useful for shopping applications, course directories, news feeds, and content libraries.
func filterProducts(
_ products: [Product],
category: String
) -> [Product] {
guard category != "All" else {
return products
}
return products.filter {
$0.category == category
}
}
The value All represents the absence of a category restriction. In a larger application, consider defining categories using an enum or stable identifier instead of relying on arbitrary strings.
8. Filtering by Availability
A filter can also use a Boolean property. For example, an application may provide an option to display only available products.
func availableProducts(
from products: [Product],
onlyAvailable: Bool
) -> [Product] {
guard onlyAvailable else {
return products
}
return products.filter {
$0.isAvailable
}
}
This function returns all records when the availability filter is disabled. When it is enabled, only records with isAvailable == true remain.
9. Combining Search and Multiple Filters
Real applications often need to combine search text with category and availability restrictions. Apply all active conditions to the original dataset so that each result satisfies every required rule.
func filterProducts(
_ products: [Product],
query: String,
category: String,
onlyAvailable: Bool
) -> [Product] {
let searchText = query.trimmingCharacters(
in: .whitespacesAndNewlines
)
return products.filter { product in
let matchesText = searchText.isEmpty ||
product.name.localizedCaseInsensitiveContains(searchText)
let matchesCategory = category == "All" ||
product.category == category
let matchesAvailability = !onlyAvailable ||
product.isAvailable
return matchesText &&
matchesCategory &&
matchesAvailability
}
}
Understanding the Conditions
matchesText checks the search query against the product name.
matchesCategory checks the selected category or allows every category.
matchesAvailability restricts the results only when the availability filter is enabled.
- The final expression uses logical AND, so a product must satisfy all active restrictions.
10. Integrating UISearchController with UITableView
For a standard UIKit search interface, attach a UISearchController to a view controller's navigation item. Configure the controller to update the displayed rows whenever the query changes.
The following complete example uses a table view controller that searches product names and filters by category. It uses the same view controller to display the original list and the filtered results.
import UIKit
final class ProductSearchViewController:
UITableViewController,
UISearchResultsUpdating {
private let products = [
Product(
name: "Wireless Headphones",
category: "Electronics",
price: 2499
),
Product(
name: "Bluetooth Speaker",
category: "Electronics",
price: 1799
),
Product(
name: "Running Shoes",
category: "Fashion",
price: 2999
),
Product(
name: "Travel Backpack",
category: "Accessories",
price: 1499
),
Product(
name: "Smart Watch",
category: "Electronics",
price: 3999,
isAvailable: false
)
]
private var filteredProducts: [Product] = []
private let searchController = UISearchController(
searchResultsController: nil
)
private var selectedCategory = "All"
override func viewDidLoad() {
super.viewDidLoad()
title = "Products"
tableView.register(
UITableViewCell.self,
forCellReuseIdentifier: "ProductCell"
)
filteredProducts = products
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search products"
searchController.searchBar.autocapitalizationType = .none
navigationItem.searchController = searchController
navigationItem.hidesSearchBarWhenScrolling = false
definesPresentationContext = true
}
private var isFiltering: Bool {
let query = searchController.searchBar.text?
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
return !query.isEmpty || selectedCategory != "All"
}
private var displayedProducts: [Product] {
isFiltering ? filteredProducts : products
}
func updateSearchResults(
for searchController: UISearchController
) {
applyFilters()
}
private func applyFilters() {
let query = searchController.searchBar.text ?? ""
filteredProducts = filterProducts(
products,
query: query,
category: selectedCategory,
onlyAvailable: false
)
tableView.reloadData()
}
func tableView(
_ tableView: UITableView,
numberOfRowsInSection section: Int
) -> Int {
return displayedProducts.count
}
override func tableView(
_ tableView: UITableView,
cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "ProductCell",
for: indexPath
)
let product = displayedProducts[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = product.name
content.secondaryText = String(
format: "₹%.2f · %@",
product.price,
product.category
)
cell.contentConfiguration = content
cell.accessoryType = .disclosureIndicator
return cell
}
}
This example assumes the Product model and combined filterProducts function shown earlier are included in the project. The controller must be placed in a navigation controller if the search field is to appear in its navigation bar.
Important Implementation Details
UISearchController(searchResultsController: nil) uses the current view controller to display results.
searchResultsUpdater connects search changes to the view controller.
updateSearchResults(for:) recalculates the filtered data.
displayedProducts selects either the original or filtered dataset.
reloadData() refreshes the table after the result array changes.
definesPresentationContext = true helps control the search presentation context.
11. Adding a Category Filter with UISegmentedControl
A segmented control provides a compact way to present a small number of mutually exclusive filter choices. In a product list, the choices might be All, Electronics, Fashion, and Accessories.
private let categoryControl: UISegmentedControl = {
let control = UISegmentedControl(items: [
"All",
"Electronics",
"Fashion",
"Accessories"
])
control.selectedSegmentIndex = 0
return control
}()
override func viewDidLoad() {
super.viewDidLoad()
categoryControl.addTarget(
self,
action: #selector(categoryChanged),
for: .valueChanged
)
}
@objc private func categoryChanged() {
let index = categoryControl.selectedSegmentIndex
let categories = [
"All",
"Electronics",
"Fashion",
"Accessories"
]
selectedCategory = categories[index]
applyFilters()
}
Add the segmented control to the view hierarchy using Auto Layout or place it in a suitable table header. If this code is used with the preceding controller, retain the existing selectedCategory property and applyFilters() method.
12. Filtering by Price Range
Numerical filters help users narrow down products according to a minimum or maximum price. The same approach can be applied to ratings, dates, quantities, and other numeric properties.
func filterByPrice(
_ products: [Product],
minimumPrice: Double,
maximumPrice: Double
) -> [Product] {
guard minimumPrice <= maximumPrice else {
return []
}
return products.filter {
$0.price >= minimumPrice &&
$0.price <= maximumPrice
}
}
In a production interface, validate the range before applying it. If users can enter currency values manually, parse them carefully and use an appropriate numeric representation for financial calculations.
13. Sorting Filtered Results
Sorting changes the order of matching records without changing which records match. Common options include alphabetical order, newest first, and price from low to high.
func sortProductsByPrice(
_ products: [Product],
ascending: Bool
) -> [Product] {
return products.sorted {
ascending
? $0.price < $1.price
: $0.price > $1.price
}
}
Apply sorting after filtering when the interface should show only matching records in a chosen order. For predictable results when two products have the same price, add a secondary sorting rule such as product name.
14. Displaying Empty Search Results
An empty state informs users when no records match their search or filter selection. Without clear feedback, a blank table may appear broken or unfinished.
Common Empty-State Messages
- “No products found.”
- “No results match your search.”
- “Try a different keyword or remove some filters.”
A simple implementation can show or hide a label according to the number of displayed results.
private let emptyLabel: UILabel = {
let label = UILabel()
label.text = "No matching products found."
label.textAlignment = .center
label.textColor = .secondaryLabel
label.numberOfLines = 0
label.isHidden = true
return label
}()
private func updateEmptyState() {
let isEmpty = displayedProducts.isEmpty
emptyLabel.isHidden = !isEmpty
tableView.backgroundView = isEmpty ? emptyLabel : nil
}
Call updateEmptyState() after updating the filtered data. Ensure that the label has an appropriate frame or layout constraints for the table view's background view and that the displayed dataset is the same one used by the data source.
15. Clearing Search Text and Resetting Filters
Users should be able to clear the search query and return to the full list. Resetting filters should also restore the intended default category and sorting options.
private func resetFilters() {
selectedCategory = "All"
searchController.searchBar.text = ""
applyFilters()
}
If a segmented control or other filter control is visible, update its selected state when resetting the filters. Otherwise, the interface may show one selection while the data reflects another.
16. Searching Multiple Fields
Search functionality can be extended to match more than one property. For example, a product directory may allow users to search by product name or category.
func searchProductsAcrossFields(
_ products: [Product],
query: String
) -> [Product] {
let searchText = query.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !searchText.isEmpty else {
return products
}
return products.filter { product in
product.name.localizedCaseInsensitiveContains(searchText) ||
product.category.localizedCaseInsensitiveContains(searchText)
}
}
For a contact list, the same approach can match a person's name, company, or email address. Decide which fields are searchable and avoid including sensitive fields unless the feature specifically requires them.
17. Searching Large Datasets
Filtering an in-memory array is convenient for small and moderate datasets. For very large collections, repeatedly scanning every record on the main thread may make the interface unresponsive.
Performance Recommendations
- Keep the data source separate from the displayed results.
- Apply efficient database queries when records are stored in a database.
- Consider debouncing rapid keystrokes for expensive search operations.
- Perform suitable expensive work away from the main thread, then update UIKit on the main thread.
- Cancel obsolete requests when a newer query replaces them.
- For server-side search, handle loading states, network failures, and pagination.
- Use stable identifiers when updating table or collection data.
- Measure performance with realistic dataset sizes instead of relying only on small examples.
For asynchronous search, make sure a slow response to an earlier query cannot overwrite the results for a newer query. A request identifier or cancellation mechanism can help manage this situation.
18. Search with Core Data or SwiftData
When an application stores many records locally, it may be more efficient to fetch matching records from persistent storage rather than load the entire dataset into memory and filter it repeatedly.
Core Data
Core Data supports fetch requests and predicates that can restrict the records returned by a fetch. This is useful when the application manages a large collection of persistent objects.
let request: NSFetchRequest<ProductEntity> =
ProductEntity.fetchRequest()
request.predicate = NSPredicate(
format: "name CONTAINS[cd] %@",
searchText
)
request.sortDescriptors = [
NSSortDescriptor(
key: "name",
ascending: true
)
]
let matchingProducts = try context.fetch(request)
This example assumes a Core Data entity named ProductEntity with a string attribute named name. Adapt the entity and property names to the application's model and handle fetch errors with do/catch.
SwiftData
SwiftData can also retrieve matching models using a FetchDescriptor and a supported predicate. The exact query depends on the model properties and the application's deployment target.
import SwiftData
func fetchMatchingTasks(
context: ModelContext,
searchText: String
) throws -> [TaskItem] {
let query = searchText.trimmingCharacters(
in: .whitespacesAndNewlines
)
guard !query.isEmpty else {
return try context.fetch(
FetchDescriptor<TaskItem>()
)
}
let descriptor = FetchDescriptor<TaskItem>(
predicate: #Predicate {
$0.title.localizedStandardContains(query)
}
)
return try context.fetch(descriptor)
}
This is an illustrative SwiftData pattern. Predicate expressions must be supported by the framework and SDK in use; if a particular string operation is not supported by the predicate translator, choose a supported expression or use an appropriate alternative strategy.
19. Search Accessibility and User Experience
A well-designed search feature should be usable by people with different accessibility needs and should communicate its state clearly.
- Use a meaningful search placeholder such as “Search products”.
- Support Dynamic Type in custom result cells.
- Maintain sufficient text and background contrast.
- Make filter controls accessible to VoiceOver.
- Do not communicate availability or selection using color alone.
- Provide a clear way to remove the query or reset filters.
- Display helpful empty and loading states.
- Preserve the user's search state when appropriate during navigation.
- Use localized text comparisons and localized messages where suitable.
20. Common Problems and Troubleshooting
| Problem |
Possible Cause |
Solution |
| Search results do not update. |
The search controller has no results updater or the update method does not refresh the data. |
Assign searchResultsUpdater and verify the filtering method. |
| Search returns no results unexpectedly. |
The query contains extra spaces or the filter checks the wrong property. |
Trim the query and inspect the matching conditions. |
| Only some filters work. |
The conditions are not combined correctly. |
Check each condition and the logical operators used between them. |
| Clearing search leaves a filtered list. |
The filtered array is not reset or the displayed dataset is selected incorrectly. |
Recalculate results when the query becomes empty. |
| Results appear in an unexpected order. |
Sorting is applied before later updates or uses the wrong property. |
Apply the selected sort to the current matching dataset. |
| Search becomes slow with many records. |
Every keystroke triggers expensive work on the main thread. |
Optimize filtering, debounce when appropriate, or query persistent storage. |
| Old network results replace newer results. |
Asynchronous requests complete out of order. |
Cancel obsolete requests or ignore responses for outdated queries. |
| Filter control and results disagree. |
The selected control value is not synchronized with the active filter. |
Update both the selected state and the displayed data together. |
21. Practical Project: Searchable Product Directory
Create a product directory using UIKit that lets users search by name, filter by category, show available products only, and sort by price.
Project Requirements
- Create a product model with an identifier, name, category, price, and availability.
- Display products in a
UITableView.
- Add a
UISearchController to the navigation bar.
- Implement live, case-insensitive search.
- Add category selection using a segmented control or another suitable filter interface.
- Provide an option to show only available products.
- Allow results to be sorted by price.
- Display an empty-state message when no products match.
- Test combinations of search terms and filters.
Suggested Development Steps
- Define the product model and sample dataset.
- Build the initial table view and verify that all products appear.
- Configure the search controller and update protocol.
- Implement the combined filtering function.
- Connect category and availability controls to the filter logic.
- Add sorting and empty-state handling.
- Test clearing search and resetting all filters.
- Measure responsiveness with a larger sample dataset.
22. Practice Exercises
- Build a contact list that searches by first name and last name.
- Add a category filter to a list of courses.
- Implement a price range filter for a product catalog.
- Add a sort option for ascending and descending price.
- Create an empty state that provides a reset-filters action.
- Search across product names and categories simultaneously.
- Modify a list to retrieve matching records through Core Data or SwiftData.
- Test a search feature with empty text, leading spaces, uppercase input, and no matches.
23. Interview Questions and Answers
Q1. What is the difference between searching and filtering?
Answer: Searching finds records that match a query, while filtering restricts the dataset according to selected conditions such as category, availability, or price.
Q2. What is UISearchController?
Answer: It is a UIKit controller that manages search interaction and coordinates the presentation and updating of search results.
Q3. What is UISearchResultsUpdating?
Answer: It is a protocol that provides a method for responding when search information changes so the application can update its results.
Q4. What is the role of UISearchBar?
Answer: It provides the text-entry interface through which users enter search terms and interact with search controls.
Q5. How do you implement case-insensitive search?
Answer: For an in-memory string search, a method such as localizedCaseInsensitiveContains(_:) can be used when appropriate.
Q6. How can multiple filters be combined?
Answer: Evaluate each active condition and combine them using the required logical operators. For example, a product may need to match the search text, category, and availability condition simultaneously.
Q7. Why should the original dataset be retained?
Answer: It allows every new query to be evaluated against the full available dataset instead of only the results of a previous search.
Q8. What is an empty state?
Answer: It is a user interface message or view that explains when no records match the current search or filters.
Q9. How can search performance be improved?
Answer: Optimize the filtering operation, avoid expensive work on the main thread, debounce rapid input when appropriate, and query persistent storage for large datasets.
Q10. When should a database query be used instead of in-memory filtering?
Answer: Database queries are useful when the dataset is large or already stored persistently, because they can retrieve only matching records rather than requiring the entire collection to be loaded into memory.
Q11. How should asynchronous search requests be handled?
Answer: Cancel outdated requests when possible or verify that a response still corresponds to the latest query before updating the interface.
Q12. Can a collection view also support search?
Answer: Yes. The same search and filtering logic can provide data to a collection view, which displays matching records in grids, cards, or other layouts.
24. Quick Revision Checklist
- Search: Finds records matching a query.
- Filtering: Restricts records according to conditions.
- Sorting: Changes the order of matching records.
- UISearchController: Coordinates the search interface.
- UISearchBar: Collects the search text.
- UISearchResultsUpdating: Responds to search updates.
- UITableView: Displays results in rows.
- UICollectionView: Displays results in flexible layouts.
- Combined conditions: Apply text, category, and availability rules together.
- Empty state: Explains when no results match.
- Performance: Avoid unnecessary expensive work on the main thread.
- Accessibility: Make search and filter controls understandable and usable.
25. Conclusion
Searchable and filterable lists improve the usability of iOS applications by helping users locate relevant information efficiently. UIKit provides components such as UISearchController, UISearchBar, and UITableView to build responsive search interfaces.
By separating the original data from the displayed results, combining filter conditions carefully, supporting sorting and empty states, and optimizing expensive queries, developers can create maintainable search experiences for both small and large datasets.
Practice these concepts by completing the Searchable Product Directory project and testing multiple search terms, filter combinations, and edge cases.
Explore the iOS course: iOS App Development Training at JustAcademy
Register for a demo: JustAcademy Course Demo Registration
Visit the homepage: JustAcademy